Põhjalik juhend video pakkimisalgoritmide mõistmiseks ja nullist implementeerimiseks Pythoni abil. Õpi tundma kaasaegsete videokodekite teooriat ja praktikat.
Videokodeki ehitamine Pythonis: sĂĽvauuring pakkimisalgoritmidest
Meie ülimalt ühendatud maailmas on video kuningas. Alates voogedastusteenustest ja videokonverentsidest kuni sotsiaalmeedia voogudeni domineerib digitaalne video internetiliikluses. Aga kuidas on võimalik saata kõrglahutusega film üle tavalise internetiühenduse? Vastus peitub paeluvas ja keerulises valdkonnas: video pakkimises. Selle tehnoloogia keskmes on videokodek (KOder-DEKooder), keerukas algoritmide kogum, mis on loodud faili suuruse drastiliseks vähendamiseks, säilitades samal ajal visuaalse kvaliteedi.
Kuigi tööstusstandardi kodekid nagu H.264, HEVC (H.265) ja autoritasudeta AV1 on uskumatult keerulised inseneritöö saavutused, on nende aluspõhimõtete mõistmine kättesaadav igale motiveeritud arendajale. See juhend viib teid sügavale rännakule video pakkimise maailma. Me ei räägi ainult teooriast; me ehitame lihtsustatud, haridusliku videokodeki nullist üles, kasutades Pythonit. See praktiline lähenemine on parim viis mõista elegantseid ideid, mis muudavad kaasaegse video voogedastuse võimalikuks.
Miks Python? Kuigi see pole keel, mida kasutaksite reaalajas toimiva, suure jõudlusega kommertskodeki jaoks (mis on tavaliselt kirjutatud C/C++ või isegi assemblerkeeles), teevad Pythoni loetavus ja selle võimsad teegid nagu NumPy, SciPy ja OpenCV sellest täiusliku keskkonna õppimiseks, prototüüpimiseks ja uurimistööks. Saate keskenduda algoritmidele, ilma et takerduksite madala taseme mäluhaldusesse.
Videopakkimise põhimõistete mõistmine
Enne kui kirjutame ühegi rea koodi, peame mõistma, mida me saavutada püüame. Video pakkimise eesmärk on eemaldada liiased andmed. Töötlemata, pakkimata video on kolossaalne. Üks minut 1080p videot 30 kaadrit sekundis võib ületada 7 GB. Selle tohutu andmemahuga toime tulemiseks kasutame ära kahte peamist tüüpi liiasust.
Pakkimise kaks sammast: ruumiline ja ajaline liiasus
- Ruumiline (kaadrisisene) liiasus: See on liiasus ühe kaadri sees. Mõelge suurele sinise taeva lapile või valgele seinale. Selle asemel, et salvestada iga üksiku piksli värvi väärtus selles piirkonnas, saame seda kirjeldada tõhusamalt. See on sama põhimõte, mis on pildipakkimisvormingute, nagu JPEG, taga.
- Ajaline (kaadritevaheline) liiasus: See on liiasus järjestikuste kaadrite vahel. Enamikus videotes ei muutu stseen ühelt kaadrilt teisele täielikult. Näiteks staatilise tausta ees rääkival inimesel on tohutult palju ajalist liiasust. Taust jääb samaks; ainult väike osa pildist (inimese nägu ja keha) liigub. See on video pakkimise kõige olulisem allikas.
Põhilised kaadritüübid: I-, P- ja B-kaadrid
Ajalise liiasuse ärakasutamiseks ei käsitle kodekid iga kaadrit võrdselt. Nad kategoriseerivad need eri tüüpideks, moodustades jada, mida nimetatakse pildigrupiks (Group of Pictures ehk GOP).
- I-kaader (Intra-coded Frame): I-kaader on täielik, iseseisev pilt. See pakitakse kokku, kasutades ainult ruumilist liiasust, sarnaselt JPEG-pildile. I-kaadrid toimivad ankurpunktidena videovoo, võimaldades vaatajal taasesitust alustada või uude kohta kerida. Need on suurimad kaadritüübid, kuid on video taastamiseks hädavajalikud.
- P-kaader (Predicted Frame): P-kaader kodeeritakse, vaadates eelmist I- või P-kaadrit. Kogu pildi salvestamise asemel salvestab see ainult erinevused. Näiteks salvestab see juhiseid nagu "võta see pikslite plokk eelmisest kaadrist, liiguta seda 5 pikslit paremale ja siin on väikesed värvimuutused." See saavutatakse protsessi kaudu, mida nimetatakse liikumise hindamiseks.
- B-kaader (Bi-directionally Predicted Frame): B-kaader on kõige tõhusam. See saab ennustamiseks kasutada nii eelmist kui ka järgmist kaadrit referentsina. See on kasulik stseenide puhul, kus objekt on ajutiselt peidetud ja ilmub seejärel uuesti. Ette- ja tahapoole vaadates suudab kodek luua täpsema ja andmetõhusama ennustuse. Tulevaste kaadrite kasutamine toob aga kaasa väikese viivituse (latentsuse), muutes need vähem sobivaks reaalajas rakendustele nagu videokõned.
Tüüpiline GOP võib välja näha nii: I B B P B B P B B I .... Kodeerija otsustab optimaalse kaadrite mustri, et tasakaalustada pakkimise tõhusust ja keritavust.
Pakkimise torujuhe: samm-sammuline ĂĽlevaade
Kaasaegne video kodeerimine on mitmeetapiline torujuhe. Iga etapp teisendab andmeid, et muuta need paremini pakitavaks. Vaatame läbi ühe kaadri kodeerimise peamised sammud.

1. samm: Värviruumi teisendamine (RGB -> YCbCr)
Enamik videoid algab RGB (punane, roheline, sinine) värviruumis. Inimsilm on aga palju tundlikum heleduse (luma) muutustele kui värvi (kroma) muutustele. Kodekid kasutavad seda ära, teisendades RGB luma/kroma vormingusse nagu YCbCr.
- Y: Luma komponent (heledus).
- Cb: Sinise erinevuse kroma komponent.
- Cr: Punase erinevuse kroma komponent.
Eraldades heleduse värvist, saame rakendada kroma aladiskreetimist. See tehnika vähendab värvikanalite (Cb ja Cr) eraldusvõimet, säilitades samal ajal heleduse kanali (Y) täieliku eraldusvõime, millele meie silmad on kõige tundlikumad. Levinud skeem on 4:2:0, mis heidab kõrvale 75% värviinformatsioonist peaaegu ilma tajutava kvaliteedikaota, saavutades kohese pakkimise.
2. samm: Kaadri jaotamine (makroplokid)
Kodeerija ei töötle tervet kaadrit korraga. See jagab kaadri väiksemateks plokkideks, tavaliselt 16x16 või 8x8 pikslit, mida nimetatakse makroplokkideks. Kõik järgnevad töötlemisetapid (ennustamine, teisendamine jne) tehakse plokipõhiselt.
3. samm: Ennustamine (Inter ja Intra)
Siin toimub maagia. Iga makroploki jaoks otsustab kodeerija, kas kasutada kaadrisisest või kaadritevahelist ennustust.
- I-kaadri puhul (Intra-ennustus): Kodeerija ennustab praegust plokki selle juba kodeeritud naabrite (ülal ja vasakul asuvate plokkide) pikslite põhjal samas kaadris. Seejärel peab see kodeerima ainult väikese erinevuse (jäägi) ennustuse ja tegeliku ploki vahel.
- P- või B-kaadri puhul (Inter-ennustus): See on liikumise hindamine. Kodeerija otsib sobivat plokki referentskaadrist. Kui see leiab parima vaste, salvestab see liikumisvektori (nt "liiguta 10 pikslit paremale, 2 pikslit alla") ja arvutab jäägi. Sageli on jääk nullilähedane, nõudes kodeerimiseks väga vähe bitte.
4. samm: Teisendamine (nt diskreetne koosinusteisendus - DCT)
Pärast ennustamist on meil jääkplokk. See plokk lastakse läbi matemaatilise teisenduse, nagu diskreetne koosinusteisendus (DCT). DCT ise andmeid ei paki, kuid see muudab põhjalikult nende esitusviisi. See teisendab ruumilised piksliväärtused sageduskoefitsientideks. DCT võlu seisneb selles, et enamiku looduslike piltide puhul koondab see suurema osa visuaalsest energiast vaid mõnesse koefitsienti ploki ülemisse vasakusse nurka (madalsageduslikud komponendid), samas kui ülejäänud koefitsiendid (kõrgsageduslik müra) on nullilähedased.
5. samm: Kvantimine
See on peamine kadudega samm torujuhtmes ja võti kvaliteedi ja bitikiiruse kompromissi kontrollimiseks. Teisendatud DCT-koefitsientide plokk jagatakse kvantimismaatriksiga ja tulemused ümardatakse lähima täisarvuni. Kvantimismaatriksil on suuremad väärtused kõrgsageduslike koefitsientide jaoks, surudes paljud neist nulli. Siin visatakse ära tohutu hulk andmeid. Kõrgem kvantimisparameeter toob kaasa rohkem nulle, suurema pakkimise ja madalama visuaalse kvaliteedi (sageli nähtav plokiliste artefaktidena).
6. samm: Entroopia kodeerimine
Viimane etapp on kadudeta pakkimise samm. Kvantitud koefitsiendid, liikumisvektorid ja muud metaandmed skaneeritakse ja teisendatakse binaarvoogu. Kasutatakse tehnikaid nagu jadakodeerimine (Run-Length Encoding - RLE) ja Huffman'i kodeerimine või keerukamaid meetodeid nagu CABAC (Context-Adaptive Binary Arithmetic Coding). Need algoritmid määravad lühemad koodid sagedamini esinevatele sümbolitele (nagu kvantimisega loodud paljud nullid) ja pikemad koodid harvemini esinevatele, pigistades andmevoost välja viimased bitid.
Dekooder teostab need sammud lihtsalt vastupidises järjekorras: entroopia dekodeerimine -> pöördkvantimine -> pöördteisendus -> liikumise kompenseerimine -> kaadri rekonstrueerimine.
Lihtsustatud videokodeki implementeerimine Pythonis
Nüüd viime teooria praktikasse. Ehitame haridusliku kodeki, mis kasutab I- ja P-kaadreid. See demonstreerib põhilist torujuhet: liikumise hindamine, DCT, kvantimine ja vastavad dekodeerimisetapid.
Hoiatus: See on *õppekodek*, mis on mõeldud õppimiseks. See ei ole optimeeritud ega anna H.264-ga võrreldavaid tulemusi. Meie eesmärk on näha algoritme tegevuses.
Eeltingimused
Teil on vaja järgmisi Pythoni teeke. Saate need installida pip-iga:
pip install numpy opencv-python scipy
Projekti struktuur
Organiseerime oma koodi mõnesse faili:
main.py: Peamine skript kodeerimis- ja dekodeerimisprotsessi käivitamiseks.encoder.py: Sisaldab kodeerija loogikat.decoder.py: Sisaldab dekoodri loogikat.utils.py: Abifunktsioonid video I/O ja teisenduste jaoks.
1. osa: Põhilised abivahendid (`utils.py`)
Alustame abifunktsioonidega DCT, kvantimise ja nende pöördfunktsioonide jaoks. Vaja on ka funktsiooni kaadri plokkideks jagamiseks.
# utils.py
import numpy as np
from scipy.fftpack import dct, idct
BLOCK_SIZE = 8
# A standard JPEG quantization matrix (scaled for our purposes)
QUANTIZATION_MATRIX = np.array([
[16, 11, 10, 16, 24, 40, 51, 61],
[12, 12, 14, 19, 26, 58, 60, 55],
[14, 13, 16, 24, 40, 57, 69, 56],
[14, 17, 22, 29, 51, 87, 80, 62],
[18, 22, 37, 56, 68, 109, 103, 77],
[24, 35, 55, 64, 81, 104, 113, 92],
[49, 64, 78, 87, 103, 121, 120, 101],
[72, 92, 95, 98, 112, 100, 103, 99]
])
def apply_dct(block):
"""Applies 2D DCT to a block."""
# Center the pixel values around 0
block = block - 128
return dct(dct(block.T, norm='ortho').T, norm='ortho')
def apply_idct(dct_block):
"""Applies 2D Inverse DCT to a block."""
block = idct(idct(dct_block.T, norm='ortho').T, norm='ortho')
# De-center and clip to valid pixel range
return np.round(block + 128).clip(0, 255)
def quantize(dct_block, qp=1):
"""Quantizes a DCT block. qp is a quality parameter."""
return np.round(dct_block / (QUANTIZATION_MATRIX * qp)).astype(int)
def dequantize(quantized_block, qp=1):
"""Dequantizes a block."""
return quantized_block * (QUANTIZATION_MATRIX * qp)
def frame_to_blocks(frame):
"""Splits a frame into 8x8 blocks."""
blocks = []
h, w = frame.shape
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
blocks.append(frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE])
return blocks
def blocks_to_frame(blocks, h, w):
"""Reconstructs a frame from 8x8 blocks."""
frame = np.zeros((h, w), dtype=np.uint8)
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE] = blocks[k]
k += 1
return frame
2. osa: Kodeerija (`encoder.py`)
Kodeerija on kõige keerulisem osa. Implementeerime lihtsa plokkide sobitamise algoritmi liikumise hindamiseks ja seejärel töötleme I- ja P-kaadreid.
# encoder.py
import numpy as np
from utils import apply_dct, quantize, frame_to_blocks, BLOCK_SIZE
def get_motion_vectors(current_frame, reference_frame, search_range=8):
"""A simple block matching algorithm for motion estimation."""
h, w = current_frame.shape
motion_vectors = []
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
current_block = current_frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
best_match_sad = float('inf')
best_match_vector = (0, 0)
# Search in the reference frame
for y in range(-search_range, search_range + 1):
for x in range(-search_range, search_range + 1):
ref_i, ref_j = i + y, j + x
if 0 <= ref_i <= h - BLOCK_SIZE and 0 <= ref_j <= w - BLOCK_SIZE:
ref_block = reference_frame[ref_i:ref_i+BLOCK_SIZE, ref_j:ref_j+BLOCK_SIZE]
sad = np.sum(np.abs(current_block - ref_block))
if sad < best_match_sad:
best_match_sad = sad
best_match_vector = (y, x)
motion_vectors.append(best_match_vector)
return motion_vectors
def encode_iframe(frame, qp=1):
"""Encodes an I-frame."""
h, w = frame.shape
blocks = frame_to_blocks(frame)
quantized_blocks = []
for block in blocks:
dct_block = apply_dct(block.astype(float))
quantized_block = quantize(dct_block, qp)
quantized_blocks.append(quantized_block)
return {'type': 'I', 'h': h, 'w': w, 'data': quantized_blocks, 'qp': qp}
def encode_pframe(current_frame, reference_frame, qp=1):
"""Encodes a P-frame."""
h, w = current_frame.shape
motion_vectors = get_motion_vectors(current_frame, reference_frame)
quantized_residuals = []
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
current_block = current_frame[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
mv_y, mv_x = motion_vectors[k]
ref_block = reference_frame[i+mv_y : i+mv_y+BLOCK_SIZE, j+mv_x : j+mv_x+BLOCK_SIZE]
residual = current_block.astype(float) - ref_block.astype(float)
dct_residual = apply_dct(residual)
quantized_residual = quantize(dct_residual, qp)
quantized_residuals.append(quantized_residual)
k += 1
return {'type': 'P', 'motion_vectors': motion_vectors, 'data': quantized_residuals, 'qp': qp}
3. osa: Dekooder (`decoder.py`)
Dekooder pöörab protsessi ümber. P-kaadrite jaoks teostab see liikumise kompenseerimise, kasutades salvestatud liikumisvektoreid.
# decoder.py
import numpy as np
from utils import apply_idct, dequantize, blocks_to_frame, BLOCK_SIZE
def decode_iframe(encoded_frame):
"""Decodes an I-frame."""
h, w = encoded_frame['h'], encoded_frame['w']
qp = encoded_frame['qp']
quantized_blocks = encoded_frame['data']
reconstructed_blocks = []
for q_block in quantized_blocks:
dct_block = dequantize(q_block, qp)
block = apply_idct(dct_block)
reconstructed_blocks.append(block.astype(np.uint8))
return blocks_to_frame(reconstructed_blocks, h, w)
def decode_pframe(encoded_frame, reference_frame):
"""Decodes a P-frame using its reference frame."""
h, w = reference_frame.shape
qp = encoded_frame['qp']
motion_vectors = encoded_frame['motion_vectors']
quantized_residuals = encoded_frame['data']
reconstructed_blocks = []
k = 0
for i in range(0, h, BLOCK_SIZE):
for j in range(0, w, BLOCK_SIZE):
# Decode the residual
dct_residual = dequantize(quantized_residuals[k], qp)
residual = apply_idct(dct_residual)
# Perform motion compensation
mv_y, mv_x = motion_vectors[k]
ref_block = reference_frame[i+mv_y : i+mv_y+BLOCK_SIZE, j+mv_x : j+mv_x+BLOCK_SIZE]
# Reconstruct the block
reconstructed_block = (ref_block.astype(float) + residual).clip(0, 255)
reconstructed_blocks.append(reconstructed_block.astype(np.uint8))
k += 1
return blocks_to_frame(reconstructed_blocks, h, w)
4. osa: Kõige kokkupanek (`main.py`)
See skript korraldab kogu protsessi: video lugemine, selle kaaderhaaval kodeerimine ja seejärel dekodeerimine lõppväljundi saamiseks.
# main.py
import cv2
import pickle # For saving/loading our compressed data structure
from encoder import encode_iframe, encode_pframe
from decoder import decode_iframe, decode_pframe
def main(input_path, output_path, compressed_file_path):
cap = cv2.VideoCapture(input_path)
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
# We'll work with the grayscale (luma) channel for simplicity
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))
cap.release()
# --- ENCODING --- #
print("Encoding...")
compressed_data = []
reference_frame = None
gop_size = 12 # I-frame every 12 frames
for i, frame in enumerate(frames):
if i % gop_size == 0:
# Encode as I-frame
encoded_frame = encode_iframe(frame, qp=2.5)
compressed_data.append(encoded_frame)
print(f"Encoded frame {i} as I-frame")
else:
# Encode as P-frame
encoded_frame = encode_pframe(frame, reference_frame, qp=2.5)
compressed_data.append(encoded_frame)
print(f"Encoded frame {i} as P-frame")
# The reference for the next P-frame needs to be the *reconstructed* last frame
if encoded_frame['type'] == 'I':
reference_frame = decode_iframe(encoded_frame)
else:
reference_frame = decode_pframe(encoded_frame, reference_frame)
with open(compressed_file_path, 'wb') as f:
pickle.dump(compressed_data, f)
print(f"Compressed data saved to {compressed_file_path}")
# --- DECODING --- #
print("\nDecoding...")
with open(compressed_file_path, 'rb') as f:
loaded_compressed_data = pickle.load(f)
decoded_frames = []
reference_frame = None
for i, encoded_frame in enumerate(loaded_compressed_data):
if encoded_frame['type'] == 'I':
decoded_frame = decode_iframe(encoded_frame)
print(f"Decoded frame {i} (I-frame)")
else:
decoded_frame = decode_pframe(encoded_frame, reference_frame)
print(f"Decoded frame {i} (P-frame)")
decoded_frames.append(decoded_frame)
reference_frame = decoded_frame
# --- WRITING OUTPUT VIDEO --- #
h, w = decoded_frames[0].shape
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, 30.0, (w, h), isColor=False)
for frame in decoded_frames:
out.write(frame)
out.release()
print(f"Decoded video saved to {output_path}")
if __name__ == '__main__':
main('input.mp4', 'output.mp4', 'compressed.bin')
Tulemuste analĂĽĂĽs ja edasised uurimissuunad
Pärast `main.py` skripti käivitamist `input.mp4` failiga saate kaks faili: `compressed.bin`, mis sisaldab meie kohandatud pakitud videoandmeid, ja `output.mp4`, rekonstrueeritud video. Võrrelge `input.mp4` suurust `compressed.bin` suurusega, et näha pakkimissuhet. Kontrollige visuaalselt `output.mp4`, et näha kvaliteeti. Tõenäoliselt näete plokilisi artefakte, eriti kõrgema `qp` väärtusega, mis on klassikaline kvantimise märk.
Kvaliteedi mõõtmine: tipp-signaali-müra suhe (PSNR)
Levinud objektiivne mõõdik rekonstrueerimise kvaliteedi mõõtmiseks on PSNR. See võrdleb originaalkaadrit dekodeeritud kaadriga. Kõrgem PSNR näitab üldiselt paremat kvaliteeti.
import numpy as np
import math
def calculate_psnr(original, compressed):
mse = np.mean((original - compressed) ** 2)
if mse == 0:
return float('inf')
max_pixel = 255.0
psnr = 20 * math.log10(max_pixel / math.sqrt(mse))
return psnr
Piirangud ja järgmised sammud
Meie lihtne kodek on suurepärane algus, kuid see on kaugel täiuslikkusest. Siin on mõned piirangud ja potentsiaalsed täiustused, mis peegeldavad pärismaailma kodekite arengut:
- Liikumise hindamine: Meie ammendav otsing on aeglane ja lihtne. Päris kodekid kasutavad keerukaid, hierarhilisi otsingualgoritme, et leida liikumisvektoreid palju kiiremini.
- B-kaadrid: Implementeerisime ainult P-kaadrid. B-kaadrite lisamine parandaks oluliselt pakkimise tõhusust, kuid suurendaks keerukust ja latentsust.
- Entroopia kodeerimine: Me ei implementeerinud korralikku entroopia kodeerimise etappi. Me lihtsalt salvestasime Pythoni andmestruktuurid pickle'i abil. Jadakodeerija lisamine kvantitud nullide jaoks, millele järgneb Huffmani või aritmeetiline kodeerija, vähendaks faili suurust veelgi.
- Plokkide eemaldamise filter (Deblocking Filter): Teravad servad meie 8x8 plokkide vahel põhjustavad nähtavaid artefakte. Kaasaegsed kodekid rakendavad pärast rekonstrueerimist plokkide eemaldamise filtrit, et neid servi siluda ja visuaalset kvaliteeti parandada.
- Muutuvad plokisuurused: Kaasaegsed kodekid ei kasuta ainult fikseeritud 16x16 makroplokke. Nad suudavad kaadrit kohanduvalt jaotada erineva suuruse ja kujuga plokkideks, et paremini sobituda sisuga (nt kasutades suuremaid plokke lamedate alade ja väiksemaid plokke detailsete alade jaoks).
Kokkuvõte
Videokodeki ehitamine, isegi lihtsustatud kujul, on sügavalt rahuldust pakkuv tegevus. See muudab arusaadavamaks tehnoloogia, mis toidab olulist osa meie digitaalsest elust. Oleme rännanud läbi ruumilise ja ajalise liiasuse põhimõistete, läbinud kodeerimise torujuhtme olulised etapid – ennustamine, teisendamine ja kvantimine – ning implementeerinud need ideed Pythonis.
Siin esitatud kood on lähtepunkt. Julgustan teid sellega katsetama. Proovige muuta ploki suurust, kvantimisparameetrit (`qp`) või GOP pikkust. Proovige implementeerida lihtsat jadakodeerimise skeemi või isegi võtta vastu väljakutse lisada B-kaadrid. Asju ehitades ja katsetades saate sügava tunnustuse leidlikkuse vastu, mis peitub sujuvates videokogemustes, mida me sageli enesestmõistetavaks peame. Video pakkimise maailm on lai ja pidevalt arenev, pakkudes lõputuid võimalusi õppimiseks ja innovatsiooniks.